/*
 * PopupUpdateManager.cpp
 *
 *  Created on: Nov 20, 2012
 *      Author: guillou
 */
#include <string>
#include <sstream>
#include <PopupOSAL.hpp>
#include <PopupUpdateManager.hpp>
#include <PopupSettings.hpp>
#include <PopupUtilities.hpp>
#include <PopupVersion.hpp>
#include <PopupGuiCore.hpp>

using namespace std;
using namespace PopupUtils;

#define KEY_VERSION    "version"
#define KEY_EXECUTABLE "exe"
#define KEY_REQUIRES   "requires"
#define KEY_CHANGES    "changes"

PopupUpdateManager::PopupUpdateManager()
: m_curl(0), m_update(0), m_exit(false), m_connected(false)
{
  connect(&m_timer, SIGNAL(timeout()), this, SLOT(slot_launchUpdate()));
}

PopupUpdateManager::~PopupUpdateManager()
{
  m_mutex.lock();
  if (m_update != 0) {
    delete m_update;
  }
  m_mutex.unlock();
}

void PopupUpdateManager::run()
{
  bool _exit = false;
  m_curl = curl_easy_init();

  if (m_curl == 0)
  {
    error("Ooops! Failed to init update manager (curl init KO).");
    return;
  }
  else
  {
    info("Update manager thread started.");
  }

  m_mutex.lock();
  _exit = m_exit;
  m_mutex.unlock();

  while (!_exit)
  {
    // URL to get update from
    string _updateInfoFileURL =
        PopupSettings::instance()->updateUrl().toStdString() +
        "/popup-" + PopupBuildInfo::target() + ".txt";

    // Path of the update info file, when downloaded to local host
    string _updateInfoFilePath =
        PopupSettings::instance()->resourceDirectory().toStdString() +
        Defs::FileSeparator + "update.txt";

    // Attributes read from update info file
    float _newExeVersion = 0.0;
    float _referenceVersion = (m_update?
        m_update->version() : PopupBuildInfo::version());
    string _newExeURL = "";
    vector<string> _newExeDepsURLs;
    QString _changes = "";

    // Download info file and parse
    bool _rc =
        downloadFile(_updateInfoFileURL, _updateInfoFilePath) &&
        parseUpdateInfoFile(_updateInfoFilePath, _newExeURL,
                            _newExeVersion, _newExeDepsURLs,
                            _changes);

    // Update required!
    if (_rc && (_newExeVersion > _referenceVersion))
    {
      // Create a temporary update
      PopupUpdate *_update = new PopupUpdate(_newExeVersion);

      // Create update basedir
      _rc = PopupUtils::mkdir(_update->basedir());

      // Download new executable
      if (PopupUtils::basename(_newExeURL).compare(_newExeURL) == 0) {
        _newExeURL = PopupSettings::instance()->updateUrl().toStdString() +
            "/" + _newExeURL;
      }
      _rc = downloadFile(_newExeURL, _update->execPath());

      // Download all dependencies
      if (_rc) {
        if (_newExeDepsURLs.size() > 0) {
          vector<string>::iterator _it = _newExeDepsURLs.begin();
          while (_rc && _it != _newExeDepsURLs.end())
          {
            string _url = *_it;
            string _destFile = _update->addDep(PopupUtils::basename(*_it));
            if (PopupUtils::basename(_url).compare(_url) == 0) {
              _url = PopupSettings::instance()->updateUrl().toStdString() +
                  "/" + _url;
            }
            _rc = downloadFile(_url, _destFile);
            _it++;
          }
        }
      }

      if (_rc)
      {
        // All download operations succeeded! A new update is available
        m_mutex.lock();
        if (m_update != 0) {
          delete m_update;
        }
        m_update = _update;
        m_mutex.unlock();

        // Propose update to user... We'll be called through launchUpdate
        // slot when user will agree for update
        emit signal_updateAvailable(_newExeVersion, _changes);
      }
    }

    // Sleep til next try
    info("Next update attempt by %d hour(s)",
         PopupSettings::instance()->updateCheckDelay());
    QThread::sleep(PopupSettings::instance()->updateCheckDelay()*3600);

    m_mutex.lock();
    _exit = m_exit;
    m_mutex.unlock();
  }

  curl_easy_cleanup(m_curl);

  info("Update manager thread terminated.");
}

void PopupUpdateManager::slot_exit()
{
  m_mutex.lock();
  m_exit = true;
  m_mutex.unlock();
}

void PopupUpdateManager::slot_onConnectionUpdate(bool p_connected)
{
  m_mutex.lock();
  m_connected = p_connected;
  m_mutex.unlock();
}

void PopupUpdateManager::slot_launchUpdate()
{
  PopupUpdate *_update = 0;
  bool _connected = false;
  // Do a copy of update information in a thread safe environment
  m_mutex.lock();
  _update = m_update;
  _connected = m_connected;
  m_mutex.unlock();

  // Let's go!!
  if (_update != 0)
  {
    if (_connected == false) {

      PopupGuiCore::instance()->saveSession();

      // Replace current executable instance
      PopupUtils::replaceCurrentExecAndRestart(_update->execPath());

      //  We should never get there... Or it means exec failed!!
      emit(signal_updateFailed());

    } else {
      // Retry in 1 second
      info("Waiting for client to disconnect before starting update");
      m_timer.start(1000);
    }
  }
}

bool PopupUpdateManager::parseUpdateInfoFile(const string & p_path,
    string & p_newExeURL,
    float & p_updateVersion,
    vector<string> & p_dependencies,
    QString & p_changes)
{
  bool _rc = true;

  FileStream *_updateInfoFile = openFileStream(p_path, "r");
  if (_updateInfoFile == 0)
  {
    _rc = false;
    error("Ooops! Failed to open downloaded file: %s)",
          p_path.c_str());
  }
  else
  {
    // Parse it!
    // Make sure first line matches popup update file info header
    char _line[1024];
    memset(_line, 0, 1024);

    // Make sure first line matches popup update file info header
    if (!getLineFromStream(_line, 1024, _updateInfoFile) ||
        (strcmp(_line, "[POPUP_UPDATE_INFO]\n") != 0))
    {
      _rc = false;
      error("Update info file %s is not valid.", p_path.c_str());
    }

    if (_rc)
    {
      char *_str = NULL;
      char *_value = NULL;
      char *_key = NULL;
      p_updateVersion = PopupBuildInfo::version();
      p_newExeURL = "";

      while (getLineFromStream(_line, 1024, _updateInfoFile)) {

        _str = &_line[0];

        if ((_key = strtok(_str, "=\n")) == NULL ||
            (_value = strtok(NULL,"=\n")) == NULL)
        {
          error("Syntax error in update info file");
        }
        else if (strcmp(_key, KEY_VERSION) == 0)
        {
          QString _s = _value;
          bool _ok = false;
          float _version = _s.toFloat(&_ok);
          if (_ok) {
            p_updateVersion = _version;
          }
        }
        else if (strcmp(_key, KEY_EXECUTABLE) == 0)
        {
          p_newExeURL = string(_value);
        }
        else if (strcmp(_key, KEY_REQUIRES) == 0)
        {
          p_dependencies.push_back(string(_value));
        }
        else if (strcmp(_key, KEY_CHANGES) == 0)
        {
          p_changes = QString(_value).replace("\\n", "\n");
        }
        else {
          error("Unexpected field %s in update info file", _key);
        }
      }
    }
    closeFileStream(_updateInfoFile);
  }
  return _rc;
}

bool PopupUpdateManager::downloadFile(const string & p_URL,
                                           const string & p_downloadedFile)
{
  bool _rc = true;
  CURLcode _res;
  FileStream *_file = openFileStream(p_downloadedFile, "w");

  if (_file == 0)
  {
    _rc = false;
    error("Ooops! Failed to create file: %s)",
        p_downloadedFile.c_str());
  }
  else
  {
    // Download update info file


    curl_easy_setopt(m_curl, CURLOPT_URL, p_URL.c_str());
    curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, _file);
    if (PopupSettings::instance()->updateProxyEnabled()) {
      trace("Update proxy address is %s",
            PopupSettings::instance()->
            updateProxy().toStdString().c_str());
      curl_easy_setopt(m_curl, CURLOPT_PROXY,
                       PopupSettings::instance()->
                       updateProxy().toStdString().c_str());
    }

    // Perform the request
    info("Downloading %s", p_URL.c_str());
    _res = curl_easy_perform(m_curl);

    closeFileStream(_file);

    // Check for errors
    if (_res != CURLE_OK)
    {
      _rc = false;
      error("Failed to download %s : %s", p_URL.c_str(),
          curl_easy_strerror(_res));
    }

    long _code = 0;
    _res = curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &_code);
    if (_res != CURLE_OK)
    {
      _rc = false;
      error("Failed to get HTTP response code.");
    }
    else if (_code == 404)
    {
      _rc = false;
      error("Update executable URL is not correct (404 not found)");
    }
    else if (_code != 200)
    {
      _rc = false;
      error("Ooops! HTTP return code is %d", _code);
    }
  }
  return _rc;
}

//=============================================================================
// PopupUpdate utility class implementation
//=============================================================================

PopupUpdate::PopupUpdate(float p_version)
{
  stringstream s;
  s << PopupSettings::instance()->resourceDirectory().toStdString()
    << Defs::FileSeparator << "update_v" << p_version;

  m_version = p_version;
  m_basedir = s.str();
  m_exePath = m_basedir + Defs::FileSeparator + "executable";
}

float PopupUpdate::version() const {
  return m_version;
}

const string & PopupUpdate::basedir() const {
  return m_basedir;
}

const string & PopupUpdate::execPath() const {
  return m_exePath;
}

const vector<string> & PopupUpdate::depsPaths() const {
  return m_exeDepsPaths;
}

string PopupUpdate::addDep(const string & p_fileName) {
  string _depPath = m_basedir + Defs::FileSeparator + p_fileName;
  m_exeDepsPaths.push_back(_depPath);
  return _depPath;
}
